import inspect
import traceback
import unittest
import re
import sys
import time
from functools import wraps

import time


# from logz import log  # 需要安装logz
def time_to_string(timestamp: float) -> str:
    """时间戳转时间字符串，便于日志中更易读"""
    time_array = time.localtime(timestamp)
    time_str = time.strftime("%Y-%m-%d %H:%M:%S", time_array)
    return time_str


class TestResut(unittest.TestResult):
    def startTestRun(self):
        """整个执行开始"""
        self.start_at = time.time()  # 整个执行的开始时间
        if self.verbosity > 1:
            self._log(f'===== 测试开始, 开始时间: {time_to_string(self.start_at)} =====')

    def stopTestRun(self):
        """整个执行结束"""
        self.end_at = time.time()  # 整个执行的结束时间
        self.duration = self.end_at - self.start_at  # 整个执行的持续
        self.success = self.wasSuccessful()  # 整个执行是否成功
        if self.verbosity > 1:
            self._log(f'===== 测试结束, 持续时间: {self.duration}秒 =====')


class TestStatus(object):
    SUCCESS = 'success'
    FAIL = 'fail'
    ERROR = 'error'
    SKIPPED = 'skipped'
    XFAIL = 'xfail'
    XPASS = 'xpass'


TAG_PARTTEN = 'tag:(\w+)'
LEVEL_PARTTEN = 'level:(\d+)'


def get_case_tags(case: unittest.TestCase) -> list:
    """从用例方法的docstring中匹配出指定格式的tags"""
    case_tags = None
    case_doc = case._testMethodDoc
    if case_doc and 'tag' in case_doc:
        pattern = re.compile(TAG_PARTTEN)
        case_tags = re.findall(pattern, case_doc)
    return case_tags


def get_case_level(case: unittest.TestCase):
    """配出指定格式的level"""

    case_doc = case._testMethodDoc
    case_level = None  # todo 默认level
    if case_doc:
        pattern = re.compile(LEVEL_PARTTEN)
        levels = re.findall(pattern, case_doc)
        if levels:
            case_level = levels[0]
            try:
                case_level = int(case_level)
            except:
                raise ValueError(f'用例中level设置：{case_level} 应为整数格式')
    return case_level


def inspect_code(test):
    test_method = getattr(test.__class__, test._testMethodName)
    try:
        code = inspect.getsource(test_method)
    except Exception as ex:
        # log.exception(ex)
        print(f"ex:{ex}")
        code = ''
    return code


class TestCaseResult(object):
    """用例测试结果"""

    def __init__(self, test: unittest.case.TestCase, name=None):
        self.test = test  # 测试用例对象
        self.name = name or test._testMethodName  # 支持传入用例别名，unittest.TestCase自带属性方法
        self.id = test.id()  # 用例完整路径，unittest.TestCase自带方法
        self.description = test.shortDescription()  # 用例简要描述，unittest.TestCase自带方法
        self.doc = test._testMethodDoc  # 用例docstring，，unittest.TestCase自带属性方法
        self.module_name = test.__module__  # 用例所在模块名
        self.class_name = test.__class__.__name__  # 用例所在类名
        self.class_id = f'{test.__module__}.{test.__class__.__name__}'  # 用例所在类完整路径
        self.class_doc = test.__class__.__doc__  # 用例所在类docstring描述
        self.tags = get_case_tags(test)  # 获取用例tags
        self.level = get_case_level(test)  # 获取用例level等级
        self.code = inspect_code(test)  # 获取用例源代码
        # 用例执后更新的信息
        self.start_at = None  # 用例开始时间
        self.end_at = None  # 用例结束时间
        self.duration = None  # 用例执行持续时间
        self.status = None  # 用例测试状态
        self.output = None  # 用例内的print信息
        self.exc_info = None  # 用例异常信息
        self.reason = None  # 跳过,失败,出错原因

    @property
    def data(self):  # 组合字典格式的用例结果数据
        data = dict(
            name=self.name,
            id=self.id,
            description=self.description,
            status=self.status,
            tags=self.tags,
            level=self.level,
            time=dict(
                start_at=self.start_at,
                end_at=self.end_at,
                duration=self.duration
            ),
            class_name=self.class_name,
            class_doc=self.class_doc,
            module_name=self.module_name,
            code=self.code,
            output=self.output,
            exc_info=self.exc_info,
            reason=self.reason,
        )
        return data


# ------------------------------------------------------------


def failfast(method):
    @wraps(method)
    def inner(self, *args, **kw):
        if getattr(self, 'failfast', False):
            self.stop()
        return method(self, *args, **kw)

    return inner


def get_platform_info():
    pass


class TestResultC(unittest.TestResult):
    """定制的测试结果类,补充用例运行时间等更多的执行信息"""

    def __init__(self, stream=None, descriptions=None, verbosity=None):
        super().__init__(stream, descriptions, verbosity)  # 调用父类方法，继承父类的初始化属性，然后再进行扩充
        # 对父类的默认熟悉做部分修改
        self.testcase_results = []  # 所有用例测试结果对象（TestCaseResult对象）列表
        self.successes = []  # 成功用例对象列表，万一用得着呢
        self.verbosity = verbosity or 1  # 设置默认verbosity为1
        self.buffer = True  # 在本定制方法中强制使用self.buffer=True，缓存用例输出
        self.name = None  # 提供通过修改result对象的name属性为结果提供名称描述
        self.start_at = None
        # self.start_at = time.time()
        self.end_at = None
        self.duration = None
        # 由于继承的父类属性中存在failures、errors等属性（存放失败和异常的用例列表），此处加以区分
        self.successes_count = 0  # 成功用例数
        self.failures_count = 0  # 失败用例数
        self.errors_count = 0  # 异常用例数
        self.skipped_count = 0  # 跳过用例数
        self.expectedFailures_count = 0  # 期望失败用例数
        self.unexpectedSuccesses_count = 0  # 非期望成功用例数
        self.know_exceptions = {}  # 已知异常字典，用于通过异常名来映射失败原因，如
        # self.know_exceptions = {'requests.exceptions.ConnectionError': '请求连接异常'}
        self._original_stdout = sys.stdout
        self._original_stderr = sys.stderr
        self._stdout_buffer = None
        self._stderr_buffer = None

    @property
    def summary(self):
        """组装结果概要, details分按运行顺序和按类组织两种结构"""
        data = dict(
            name=self.name,
            success=self.wasSuccessful(),  # 用例是否成功，父类unittest.TestResult自带方法
            stat=dict(
                testsRun=self.testsRun,
                successes=self.successes_count,
                failures=self.failures_count,
                errors=self.errors_count,
                skipped=self.skipped_count,
                expectedFailures=self.expectedFailures_count,
                unexpectedSuccesses=self.unexpectedSuccesses_count,
            ),
            time=dict(
                start_at=self.start_at,
                end_at=self.end_at,
                duration=self.duration
            ),
            platform=get_platform_info(),
            details=[item.data for item in self.testcase_results]  # 每个测试用例结果对象转为其字典格式的数据
        )
        return data

    def _is_relevant_tb_level(self, tb):
        return '__unittest' in tb.tb_frame.f_globals

    def _count_relevant_tb_levels(self, tb):
        length = 0
        while tb and not self._is_relevant_tb_level(tb):
            length += 1
            tb = tb.tb_next
        return length

    # 从异常中提取异常信息方法
    def _exc_info_to_string(self, err, test):
        """重写父类的转换异常方法, 去掉buffer的输出"""
        exctype, value, tb = err
        while tb and self._is_relevant_tb_level(tb):
            tb = tb.tb_next
        if exctype is test.failureException:
            # Skip assert*() traceback levels
            length = self._count_relevant_tb_levels(tb)
        else:
            length = None
        tb_e = traceback.TracebackException(
            exctype, value, tb, limit=length, capture_locals=self.tb_locals)
        msgLines = list(tb_e.format())
        return ''.join(msgLines)

    #  从异常和已知异常中提取失败原因的方法
    def _get_exc_msg(self, err):
        exctype, value, tb = err
        exc_msg = str(value)
        exc_full_path = f'{exctype.__module__}.{exctype.__name__}'
        if self.know_exceptions and isinstance(self.know_exceptions, dict):
            exc_msg = self.know_exceptions.get(exc_full_path, exc_msg)
        return exc_msg

    def _restoreStdout(self):
        """重写父类的_restoreStdout方法并返回output+error"""
        if self.buffer:
            output = error = ''
            if self._mirrorOutput:
                output = sys.stdout.getvalue()
                error = sys.stderr.getvalue()
                # 去掉了对原始输出流的信息输出
                sys.stdout = self._original_stdout
                sys.stderr = self._original_stderr
                self._stdout_buffer.seek(0)
                self._stdout_buffer.truncate()
                self._stderr_buffer.seek(0)
                self._stderr_buffer.truncate()
                return output + error or None  # 改为return字符串，之后再log输出

    def startTestRun(self):
        """整个执行开始"""
        self.start_at = time.time()  # 整个执行的开始时间
        if self.verbosity > 1:
            self._log(f'===== 测试开始, 开始时间: {time_to_string(self.start_at)} =====')

    def stopTestRun(self):
        """整个执行结束"""
        self.end_at = time.time()  # 整个执行的结束时间
        self.duration = self.end_at - self.start_at  # 整个执行的持续
        self.success = self.wasSuccessful()  # 整个执行是否成功
        if self.verbosity > 1:
            self._log(f'===== 测试结束, 持续时间: {self.duration}秒 =====')

    # 用例开始和结束方法
    def startTest(self, test: unittest.case.TestCase):
        """单个用例执行开始"""
        super().startTest(test)  # 调用父类方法
        test.result = TestCaseResult(test)  # 实例化用例结果对象来记录用例结果，并绑定用例的result属性
        self.testcase_results.append(test.result)  # 另外添加到所有的结果列表一份
        test.result.start_at = time.time()  # 记录用例开始时间
        if self.verbosity > 1:
            self._log(
                f'执行用例: {test.result.name}: {test.result.description}, 开始时间: {time_to_string(test.result.start_at)}')

    def stopTest(self, test: unittest.case.TestCase) -> None:
        """单个用例结束"""
        test.result.end_at = time.time()  # 记录用例结束时间
        test.result.duration = test.result.end_at - test.result.start_at  # 记录用例持续时间
        # 由于output要从_restoreStdout获取，手动加入父类恢复输出流的方法
        test.result.output = self._restoreStdout()
        self._mirrorOutput = False  # 是否重定向输出流标志

    # 用例结果注册
    def addSuccess(self, test):
        """重写父类方法, 单个用例成功时在stopTest前调用"""
        test.result.status = TestStatus.SUCCESS
        self.successes.append(test)
        self.successes_count += 1
        super().addSuccess(test)

    @failfast
    def addFailure(self, test, err):
        """重写父类方法, 用例失败时在stopTest前调用"""
        test.result.status = TestStatus.FAIL
        test.result.exc_info = self._exc_info_to_string(err, test)
        test.result.reason = self._get_exc_msg(err)
        self.failures_count += 1
        super().addFailure(test, err)

    @failfast
    def addError(self, test, err):
        """重写父类方法, 用例异常时在stopTest前调用"""
        test.result.status = TestStatus.ERROR
        test.result.exc_info = self._exc_info_to_string(err, test)
        test.result.reason = self._get_exc_msg(err)
        self.errors_count += 1
        super().addError(test, err)

    def addSkip(self, test, reason):
        """重写父类方法, 用例跳过时在stopTest前调用"""
        test.result.status = TestStatus.SKIPPED
        test.result.reason = reason
        self.skipped_count += 1
        super().addSkip(test, reason)

    def addExpectedFailure(self, test, err):
        """重写父类方法, 用例期望失败时在stopTest前调用"""
        test.result.status = TestStatus.XFAIL
        test.result.exc_info = self._exc_info_to_string(err, test)
        test.result.reason = self._get_exc_msg(err)
        self.expectedFailures_count += 1
        super().addExpectedFailure(test, err)

    @failfast
    def addUnexpectedSuccess(self, test):
        """重写父类方法, 用例非期望成功时在stopTest前调用"""
        test.result.status = TestStatus.XPASS
        self.expectedFailures_count += 1
        super().addUnexpectedSuccess(test)


if __name__ == '__main__':
    # from l_unittest.UnitResult.result import TestResult

    # class TestDemo(unittest.TestCase):
    #     def test_a(self):  # 可以添加更多的用例进行测试
    #         print('测试a')
    # suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDemo)

    # 加载整个文件夹
    # test_dir = '../testCase'
    # suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')

    # 加载某个指定test case
    from l_unittest.testCase.test_file import TestStringMethods

    suite = unittest.TestSuite()
    suite.addTest(TestStringMethods('test_01'))
    runner = unittest.TextTestRunner(resultclass=TestResultC)  # 使用定制的TestResult类
    result = runner.run(suite)
    print(f"result.summary:{result.summary}")  # 输出result的字典格式数据，建议使用pprint输出，需要安装pprint
